里氏替换原则（Liskov Substitution Principle）,缩写为 LSP。
它的具体描述是：

<RedSpan>子类对象（object of subtype/derived class）能够替换程序（program）中父类对象（object of base/parent class）出现的任何地方，并且保证原来程序的逻辑行为（behavior）不变及正确性不被破坏。</RedSpan>

## 和多态的区别
从定义描述和代码实现上看，多态和里氏替换有点类似，但它们关注的角度是不一样的。

多态是面向对象的一大特性，也是面向对象编程语言的一种语法。它是一种代码实现的思路。而 <RedSpan>里氏替换是一种设计原则，是用来指导继承关系中的子类该如何设计的，子类的设计要保证替换父类的时候，不改变原有程序的逻辑以及不破坏原有程序的正确性</RedSpan>。

## 按照协议来设计
里氏替换原则还有一个更加能落地、更有指导意义的描述，那就是“**按照协议来设计**”。

<RedSpan>即子类在设计的时候，要遵守父类的行为约定（或者叫协议）。父类定义了函数的行为约定，那子类可以改变函数的内部实现逻辑，但不能改变函数原有的行为约定</RedSpan>。

这里的行为约定包括：
* <RedSpan>函数声明要实现的功能</RedSpan>
* <RedSpan>对输入、输出、异常的约定</RedSpan>
* 甚至包括注释中所罗列的任何特殊说明

:::tip
定义子类和父类之间的关系，也可以替换成接口和实现类之间的关系。
:::
下面是一些违反里氏替换原则的例子。

### 子类违背父类声明要实现的功能
父类中提供的 sortOrdersByAmount() 订单排序函数，是按照金额从小到大来给订单排序的，而子类重写的这个 sortOrdersByAmount() 订单排序函数之后，是按照创建日期来给订单排序的。那子类的设计就违背里氏替换原则。

### 子类违背父类对输入、输出、异常的约定
在父类中，某个函数约定：运行出错时返回 null；获取数据为空时返回空集合（empty collection）。而子类重载函数后，实现变了，运行出错返回异常（exception），而获取不到数据返回 null。那子类的设计就违背了里氏替换原则。

在父类中，某个函数约定，输入数据可以是任意整数，但子类实现的时候，只允许输入数据是正整数，负数就抛出，也就是说，子类对输入数据的校验比父类更加严格，那子类的设计就违背了里氏替换原则。

在父类中，某个函数约定，只会抛出 ArgumentNullException 异常，那么子类的设计实现中只允许抛出 ArgumentNullException 异常，任何其它异常的抛出，都会导致子类违背里氏替换原则。


### 子类违背父类注释中所罗列的任何特殊说明

父类中定义的 withDraw() 体现函数的注释是这么写的：“用户的提现金额不得超过账户余额...”，而子类重写 withDraw() 函数之后，针对 VIP 账号实现了透支体现的功能，那这个子类的设计也是不符合里氏替换原则的。


以上便是三种典型的违背里氏替换原则的情况，除此之外，判断子类的设计实现是否违背里氏替换原则，还有一个小窍门，那就是拿父类的单元测试去验证子类的代码。如果这些单元测试失败，则有可能说明，子类的设计实现没有完全遵守父类的约定，子类有可能违背了里氏替换原则。




